/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements.  The ASF licenses this file to You
* under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.  For additional information regarding
* copyright in this work, please see the NOTICE file in the top level
* directory of this distribution.
*/
//package org.apache.abdera.model;
package org.atomserver.utils;


import java.io.Serializable;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * <p>Provides an implementation of the Atom Date Construct,
 * which is itself a specialization of the RFC3339 date-time.</p>
 * <p/>
 * <p>Accessors on this class are not synchronized.</p>
 * <p/>
 * <p>Per RFC4287:</p>
 * <p/>
 * <pre>
 *  3.3.  Date Constructs
 * <p/>
 *  A Date construct is an element whose content MUST conform to the
 *  "date-time" production in [RFC3339].  In addition, an uppercase "T"
 *  character MUST be used to separate date and time, and an uppercase
 *  "Z" character MUST be present in the absence of a numeric time zone
 *  offset.
 * <p/>
 *  atomDateConstruct =
 *     atomCommonAttributes,
 *     xsd:dateTime
 * <p/>
 *  Such date values happen to be compatible with the following
 *  specifications: [ISO.8601.1988], [W3C.NOTE-datetime-19980827], and
 *  [W3C.REC-xmlschema-2-20041028].
 * <p/>
 *  Example Date constructs:
 * <p/>
 *  &lt;updated>2003-12-13T18:30:02Z&lt;/updated>
 *  &lt;updated>2003-12-13T18:30:02.25Z&lt;/updated>
 *  &lt;updated>2003-12-13T18:30:02+01:00&lt;/updated>
 *  &lt;updated>2003-12-13T18:30:02.25+01:00&lt;/updated>
 * <p/>
 *  Date values SHOULD be as accurate as possible.  For example, it would
 *  be generally inappropriate for a publishing system to apply the same
 *  timestamp to several entries that were published during the course of
 *  a single day.
 *  </pre>
 */
public final class AtomDate
        implements Cloneable,
        Serializable {

    private static final long serialVersionUID = -7062139688635877771L;

    private Date value = null;

    /**
     * Create an AtomDate using the current date and time
     */
    public AtomDate() {
        this(new Date());
    }

    /**
     * Create an AtomDate using the serialized string format (e.g. 2003-12-13T18:30:02Z).
     *
     * @param value The serialized RFC3339 date/time value
     */
    public AtomDate(String value) {
        this(parse(value));
    }

    /**
     * Create an AtomDate using a java.util.Date
     *
     * @param value The java.util.Date value
     */
    public AtomDate(Date value) {
        this.value = value;
    }

    /**
     * Create an AtomDate using a java.util.Calendar.
     *
     * @param value The java.util.Calendar value
     */
    public AtomDate(Calendar value) {
        this(value.getTime());
    }

    /**
     * Create an AtomDate using the number of milliseconds since January 1, 1970, 00:00:00 GMT
     *
     * @param value The number of milliseconds since January 1, 1970, 00:00:00 GMT
     */
    public AtomDate(long value) {
        this(new Date(value));
    }

    /**
     * Return the serialized string form of the Atom date
     *
     * @return the serialized string form of the date as specified by RFC4287
     */
    public String getValue() {
        return format(value);
    }

    /**
     * Sets the value of the Atom date using the serialized string form
     *
     * @param value The serialized string form of the date
     */
    public void setValue(String value) {
        this.value = parse(value);
    }

    /**
     * Sets the value of the Atom date using java.util.Date
     *
     * @param date A java.util.Date
     */
    public void setValue(Date date) {
        this.value = date;
    }

    /**
     * Sets the value of the Atom date using java.util.Calendar
     *
     * @param calendar a java.util.Calendar
     */
    public void setValue(Calendar calendar) {
        this.value = calendar.getTime();
    }

    /**
     * Sets the value of the Atom date using the number of milliseconds since January 1, 1970, 00:00:00 GMT
     *
     * @param timestamp The number of milliseconds since January 1, 1970, 00:00:00 GMT
     */
    public void setValue(long timestamp) {
        this.value = new Date(timestamp);
    }

    /**
     * Returns the value of this Atom Date
     *
     * @return A java.util.Date representing this Atom Date
     */
    public Date getDate() {
        return value;
    }

    /**
     * Returns the value of this Atom Date as a java.util.Calendar
     *
     * @return A java.util.Calendar representing this Atom Date
     */
    public Calendar getCalendar() {
        Calendar cal = Calendar.getInstance();
        cal.setTime(value);
        return cal;
    }

    /**
     * Returns the value of this Atom Date as the number of milliseconds since January 1, 1970, 00:00:00 GMT
     *
     * @return The number of milliseconds since January 1, 1970, 00:00:00 GMT
     */
    public long getTime() {
        return value.getTime();
    }

    @Override
    public String toString() {
        return getValue();
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((value == null) ? 0 : value.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        boolean answer = false;
        if (obj instanceof Date) {
            Date d = (Date) obj;
            answer = (this.value.equals(d));
        } else if (obj instanceof String) {
            Date d = parse((String) obj);
            answer = (this.value.equals(d));
        } else if (obj instanceof Calendar) {
            Calendar c = (Calendar) obj;
            answer = (this.value.equals(c.getTime()));
        } else if (obj instanceof AtomDate) {
            Date d = ((AtomDate) obj).value;
            answer = (this.value.equals(d));
        }
        return answer;
    }

    @Override
    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }

    private static final Pattern PATTERN = Pattern.compile(
            "((?:19|20)\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])?(?:[Tt](?:(\\d{2}))?(?::(\\d{2}))?(?::(\\d{2}))?(?:\\.(\\d{3}))?)?([Zz])?(?:([+-])(\\d{2}):(\\d{2}))?");

    /**
     * Parse the serialized string form into a java.util.Date
     *
     * @param date The serialized string form of the date
     * @return The created java.util.Date
     */
    public static Date parse(String date) {
        Matcher m = PATTERN.matcher(date);
        if (m.find()) {
            Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
            int hoff = 0, moff = 0, doff = -1;
            if (m.group(9) != null) {
                doff = m.group(9).equals("-") ? 1 : -1;
                hoff = doff * (m.group(10) != null ? Integer.parseInt(m.group(10)) : 0);
                moff = doff * (m.group(11) != null ? Integer.parseInt(m.group(11)) : 0);
            }
            c.set(Calendar.YEAR, Integer.parseInt(m.group(1)));
            c.set(Calendar.MONTH, m.group(2) != null ? Integer.parseInt(m.group(2)) - 1 : 0);
            c.set(Calendar.DATE, m.group(3) != null ? Integer.parseInt(m.group(3)) : 1);
            c.set(Calendar.HOUR_OF_DAY, m.group(4) != null ? Integer.parseInt(m.group(4)) + hoff : 0);
            c.set(Calendar.MINUTE, m.group(5) != null ? Integer.parseInt(m.group(5)) + moff : 0);
            c.set(Calendar.SECOND, m.group(6) != null ? Integer.parseInt(m.group(6)) : 0);
            c.set(Calendar.MILLISECOND, m.group(7) != null ? Integer.parseInt(m.group(7)) : 0);
            return c.getTime();
        } else {
            throw new IllegalArgumentException("Invalid Date Format");
        }
    }

    /**
     * Create the serialized string form from a java.util.Date
     *
     * @param d A java.util.Date
     * @return The serialized string form of the date
     */
    public static String format(Date date) {
        StringBuilder sb = new StringBuilder();
        Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        c.setTime(date);
        sb.append(c.get(Calendar.YEAR));
        sb.append('-');
        int f = c.get(Calendar.MONTH);
        if (f < 9) {
            sb.append('0');
        }
        sb.append(f + 1);
        sb.append('-');
        f = c.get(Calendar.DATE);
        if (f < 10) {
            sb.append('0');
        }
        sb.append(f);
        sb.append('T');
        f = c.get(Calendar.HOUR_OF_DAY);
        if (f < 10) {
            sb.append('0');
        }
        sb.append(f);
        sb.append(':');
        f = c.get(Calendar.MINUTE);
        if (f < 10) {
            sb.append('0');
        }
        sb.append(f);
        sb.append(':');
        f = c.get(Calendar.SECOND);
        if (f < 10) {
            sb.append('0');
        }
        sb.append(f);
        sb.append('.');
        f = c.get(Calendar.MILLISECOND);
        if (f < 100) {
            sb.append('0');
        }
        if (f < 10) {
            sb.append('0');
        }
        sb.append(f);
        sb.append('Z');
        return sb.toString();
    }

    /**
     * Create a new Atom Date instance from the serialized string form
     *
     * @param value The serialized string form of the date
     * @return The created AtomDate
     */
    public static AtomDate valueOf(String value) {
        return new AtomDate(value);
    }

    /**
     * Create a new Atom Date instance from a java.util.Date
     *
     * @param value a java.util.Date
     * @return The created AtomDate
     */
    public static AtomDate valueOf(Date value) {
        return new AtomDate(value);
    }

    /**
     * Create a new Atom Date instance from a java.util.Calendar
     *
     * @param value A java.util.Calendar
     * @return The created AtomDate
     */
    public static AtomDate valueOf(Calendar value) {
        return new AtomDate(value);
    }

    /**
     * Create a new Atom Date instance using the number of milliseconds since January 1, 1970, 00:00:00 GMT
     *
     * @param value The number of milliseconds since January 1, 1970, 00:00:00 GMT
     * @return The created AtomDate
     */
    public static AtomDate valueOf(long value) {
        return new AtomDate(value);
    }
}
